class: inverse,left, middle background-image: url(data:image/png;base64,#background.png) background-size: cover <img src="data:image/png;base64,#LOGO_DIPLOMADO.png" width="500px"/> ##Módulo 3: Percepción remota, análisis masivo y GEE ###Procesamiento masivo de datos: Paralelización en R José A. Lastra<br> <a href="http://github.com/JoseLastra"> Github: JoseLastra</a><br> <a href="mailto:jose.lastra@pucv.cl"> jose.lastra@pucv.cl</a><br> .large[<b><a href="https://www.pucv.cl/uuaa/site/edic/base/port/labgrs.html">LabGRS</a> | Noviembre, 2023 </b>] <br> --- class: center,middle background-image: url(data:image/png;base64,#labgrs_logo.png) background-size: 35% --- ## Contenidos .pull-left[ 1) Análisis masivo iterativo: * loops * Familia apply 2) Paralelización: * LibrerÃa parallel * LibrerÃa foreach y doParallel ] .pull-right[ <img src="data:image/png;base64,#https://raw.githubusercontent.com/allisonhorst/stats-illustrations/main/rstats-artwork/r_rollercoaster.png" width="650px"/> © Allison Horst ] --- ## Análisis iterativo: loops -- - En muchas ocasiones, veremos la necesidad de realizar la misma tarea muchas veces para diferentes sets de datos. -- - Algunos ejemplos: reproyectar información, realizar diversos modelos lineales, cortar datos a un área de estudio, etc. -- - Los **loops** son la definición concreta de **ejecutar una tarea n cantidad de veces**. -- - Nosotros mostraremos principalmente el uso de **ciclos for** <center><img src="data:image/png;base64,#https://www.blasbenito.com/post/02_parallelizing_loops_with_r/featured.png" width="500px"/></center> <center><a href="https://www.blasbenito.com/post/02_parallelizing_loops_with_r/">©Blas M. Benito</a></center> --- ## Análisis iterativo: loops -- - Pensemos en una tarea ya realizada durante el diplomado: Apertura de archivos -- ```r # crear objeto para primer archivo df_1 <- read_csv("TABLAS_LOOP/Tmean_Cauquenes.csv") df_1 %>% head() ``` ``` ## # A tibble: 6 × 4 ## Date Year Month Tmed ## <date> <dbl> <dbl> <dbl> ## 1 1979-01-15 1979 1 19.7 ## 2 1979-02-15 1979 2 19.1 ## 3 1979-03-15 1979 3 17.4 ## 4 1979-04-15 1979 4 14.6 ## 5 1979-05-15 1979 5 12.2 ## 6 1979-06-15 1979 6 8.54 ``` -- <center><b>¿Cómo podemos abrir todos los archivos?</b></center> --- ## Análisis iterativo: recordando [Ver](https://labgrs.github.io/01_Intro_R/Intro_R.html#63) <center><img src="data:image/png;base64,#loops.png" width="800px"/></center> <center><a href="https://labgrs.github.io/01_Intro_R/Intro_R.html#63">© MatÃas Olea</a></center> -- - Construyamos el ciclo que abra todos los archivos y los ponga en un objeto cada uno. -- - Usaremos una **lista** como estructura receptora. --- ## Análisis iterativo: loops ```r lista_archivos <- list.files(path = "TABLAS_LOOP/", # ruta de los archivos pattern = "*.csv", # patron para listar elementos full.names = T) # ruta completa lista_df <- vector(mode = "list", # lista vacia para ingresar cada tabla individual length = length(lista_archivos)) for (i in 1:length(lista_archivos)) { # seteando duracion del loop lista_df[[i]] <- read_csv(lista_archivos[i], # metiendo el objeto en la lista show_col_types = FALSE) cat("tabla", i, "leida \n") # control de ansiedad } ``` ``` ## tabla 1 leida ## tabla 2 leida ## tabla 3 leida ## tabla 4 leida ## tabla 5 leida ## tabla 6 leida ## tabla 7 leida ## tabla 8 leida ## tabla 9 leida ## tabla 10 leida ``` --- ## Analisis iterativo: loops -- - ¿Y si queremos unir solo la columna de temperatura a una gran tabla? -- ```r id <- substr(x = lista_archivos, start = 19, stop = nchar(lista_archivos) - 4) # id's lista_df <- vector(mode = "list", # lista vacia para ingresar cada tabla individual length = length(lista_archivos)) # start loop ----- for (i in 1:length(lista_archivos)) { # seteando duracion del loop lista_df[[i]] <- read_csv(lista_archivos[i], # metiendo el objeto en la lista show_col_types = FALSE) %>% mutate(ID = id[i]) # agregando nuevo campo cat("tabla", i, "leida \ ") # control de ansiedad } ``` ``` ## tabla 1 leida tabla 2 leida tabla 3 leida tabla 4 leida tabla 5 leida tabla 6 leida tabla 7 leida tabla 8 leida tabla 9 leida tabla 10 leida ``` --- ## Análisis iterativo: loops -- - Con la lista generada y los archivos leÃdos, podemos integrar nuestra tabla -- ```r tabla_final <- lista_df %>% bind_rows() ```
--- ## Análisis iterativo: loops -- - Una vez obtenido el resultado, podemos operar como sea requerido. ```r tabla_final %>% ggplot(aes(x = Date, y = Tmed, color = ID)) + geom_line() + facet_wrap(~ID, ncol = 2) ``` --- <img src="data:image/png;base64,#DIPGEOPR_03_7_files/figure-html/unnamed-chunk-8-1.png" width="100%" /> --- ## Análisis iterativo: Familia *apply -- - En general, muchos usuarios experimentados promueven **no usar** ciclos for, principalmente por motivos de eficiencia computacional (Grolemund, 2014). -- - Aunque como vimos, son una solución rápida, fácil de escribir, debuggear y entender para cualquier programador. -- - Suelen ser algo ineficientes al momento de trabajar con un alto número de datos dada su naturaleza iterativa. -- - Aquà es donde la familia de **funciones apply** entran como una alternativa al uso de ciclos for. -- - Aunque también uno puede transformar un ciclo for en un código vectorizado ([para saber más](https://rstudio-education.github.io/hopr)) --- ## Análisis iterativo: Familia *apply -- - Lo primero que uno podrÃa cuestionarse es: **¿cuál es la diferencia entre esta familia de funciones y un ciclo for?** -- - La colección *apply viene incorporada en R y puede ser alimentada por diferentes funciones para aplicar procesos redundantes sobre diferentes objetos. -- - En esencia, estas funciones buscan que el usuario evite la construcción de ciclos. Aunque estas funciones corren un ciclo for por detrás. -- - Las funciones correspondientes a esta familia son ([Mendoza, 2018](https://bookdown.org/jboscomendoza/r-principiantes4/)): .pull-left[ * apply() * eapply() * lapply() * mapply() ] .pull-right[ * rapply() * sapply() * tapply() * vapply() ] -- - Para fines operativos, mostraremos la aplicación de **apply()** y **lapply()** con nuestros datos empleados para los análisis de clúster. ```r datos_cluster <- read_csv("Datos_cluster.csv") %>% mutate(ID = paste0("A-", c(1:500))) ``` --- ## apply() -- - Es una función aplicable sobre matrices, data frames o tibbles siguiendo la estructura: `apply(x, MARGIN, FUN, ...)` **x**: es una matriz de datos <br> **MARGIN**: corresponde a sobre qué realizaremos la operación. 1 Representa las filas y 2 representa las columnas <br> **FUN**: Función a aplicar sobre nuestros datos. <center><img src="data:image/png;base64,#apply.png" width="500px"/></center> <center>DATACAMP</center> --- ## apply() -- - Calculemos el promedio de todas nuestras columnas ```r tabla_app <- apply(datos_cluster[, 1:7], 2, FUN = mean, na.rm = T) tabla_app ``` ``` ## X Y Productivity Elevation T_min T_med ## -70.82784 -32.50630 3.18256 1642.37400 3.42342 9.88018 ## T_max ## 16.33688 ``` -- - Por defecto, apply nos entregará un vector númerico de salida. -- - Podemos hacer esto con un ciclo for, pero es menos eficiente. --- ## apply() -- - También podemos aplicar lógica **tidyverse**, donde la salida será un tibble-data.frame -- ```r tabla_tidy <- datos_cluster %>% # incoporo clusters a la tabla summarise( across( where(is.numeric), # condicion de que el campo sea numerico list(mean = mean) # lista de funciones a aplicar, si el campo es numerico ) ) ``` | X_mean| Y_mean| Productivity_mean| Elevation_mean| T_min_mean| T_med_mean| T_max_mean| |---------:|--------:|-----------------:|--------------:|----------:|----------:|----------:| | -70.82784| -32.5063| 3.18256| 1642.374| 3.42342| 9.88018| 16.33688| --- <img src="data:image/png;base64,#DIPGEOPR_03_7_files/figure-html/unnamed-chunk-13-1.png" width="100%" /> ``` ## Unit: microseconds ## expr min lq mean median uq max neval ## tidy_version 2631.2 3066.80 3592.702 3572.15 3964.20 6245.2 100 ## apply_version 227.6 298.35 360.670 355.55 406.65 625.1 100 ``` --- ## lapply() -- - La función **lapply()** es considerada un caso especial de **apply()**. -- - Su objetivo es aplicar funciones a todos los elementos presentes en una lista. -- - Esta función, es mucho más flexible que **apply()** ya que en R prácticamente todo puede ser transformado en una lista. -- - La salida de **lapply()** siempre será una lista, que puede ser transformada en otro objeto posteriormente. -- - **lapply()** es un primer paso a la paralelización de procesos complejos en R. --- ##lapply() -- - Consideremos el ejemplo dispuesto en la página 16: [**Abrir múltiples archivos**](#16) -- - La estructura con **lapply()** serÃa la siguiente: ```r lista_df <- list.files( path = "TABLAS_LOOP/", pattern = "*.csv", full.names = T ) %>% lapply(read_csv, show_col_types = F) ``` ``` ## Unit: milliseconds ## expr min lq mean median uq max neval ## for_version 109.3332 114.3365 122.1638 118.3519 124.6116 287.7009 100 ## lapply_version 104.9961 108.9588 114.2227 111.6805 116.6344 153.9534 100 ``` --- <img src="data:image/png;base64,#DIPGEOPR_03_7_files/figure-html/unnamed-chunk-16-1.png" width="100%" /> -- - Como se aprecia, ganamos algo de velocidad en el procesamiento y disminuÃmos de forma importante la cantidad de lÃneas de código para realizar la misma tarea. --- ## Paralelización: Conceptos -- - Entonces, si podemos lograr el procesamiento masivo con las funciones o ejemplos vistos antes...**¿Por qué paralelizar?** <center><img src="data:image/png;base64,#cpu.gif" width="550px"/></center> <center>© Andreas Angourakis</center> -- - Los procesos de serialización solo aprovechan uno de los núcleos de nuestros computadores. --- ## Paralelización: Conceptos -- - En general, la mayorÃa de los códigos en R funcionan rápido y bien en un solo procesador o núcleo. -- - Sin embargo, a veces podemos encontrar ciertos problemas de cálculo vinculados a ([Jones, 2017](https://nceas.github.io/oss-lessons/parallel-computing-in-r/parallel-computing-in-r.html)): * **CPU**: proceso toma mucho tiempo de CPU. * **Memoria**: Demanda demasiada memoria. * **I/O**: toma mucho tiempo la lectura/escritura desde el disco. * **Network**: La red toma demasiado tiempo en transferir. -- - La paralelización viene a "solucionar" los problemas asociados a la CPU, dada la presencia de múltiples núcleos en los pc's modernos. -- - Los cuales, a su vez, tienen más memoria disponible para trabajar. --- ## Paralelización: Conceptos -- - Existen variadas librerÃas para ejecutar procesos paralelizando en R ([**ver más**](https://cran.r-project.org/web/views/HighPerformanceComputing.html)) -- - Estas en general se valen de los siguientes métodos de paralelización: * **Fork**: que copia el proceso de R a un nuevo núcleo, compartiendo el entorno. **No está disponible para Windows** * **Socket**: Lanza una versión nueva de R en cada uno de los núcleos de procesamiento. Similar a trabajar en un cluster de equipos conectados en red. --- ## Paralelización: Conceptos <center><img src="data:image/png;base64,#sock_fork.png" width="900px"/></center> <center>Fuente: Benito, 2021</center> --- ## Paralelización: Conceptos -- - Para ambos métodos podemos encontrar pros y contras: .pull-left[ **Socket** * Pro: Funciona en cualquier sistema operativo * Pro: No hay contaminación, porque cada nodo es individual * Con: es más lento que el método **Fork** * Con: Las variables, paquetes y demás, deben ser explÃcitamente puestos en cada nodo * Con: más difÃcil de implementar ] .pull-right[ **Fork** * Pro: Más rápido que el **Socket** * Pro: Al copiar la versión de R existente, todo el entorno existe en cada nodo * Pro: Más simple de implementar * Con: Solo funciona en sistemas POSIX (Max, Linux, Unix, BSD) * Con: Al operar sobre procesos duplicados, puede causar comportamientos extraños en ciertas ejecuciones. ] <br> -- - Nosotros nos enfocaremos en el uso de **Socket** en esta sesión. --- ## Paralelización: Aplicación k-means. - Para ejemplificar el proceso de paralelización, usaremos los datos del archivo **Datos_cluster.csv** leÃdos y guardados en el objeto **datos_cluster**. -- ```r estaciones_cluster <- datos_cluster %>% na.omit() # limpiar filas con NA's ```
--- ## Paralelización: Aplicación k-means. -- - Preparación de datos ```r #### normalizacion de los datos ---- estaciones_scale <- estaciones_cluster %>% dplyr::select(-c(ID, X, Y)) # sacamos coordenadas e ID estaciones_scale <- scale(estaciones_scale) rownames(estaciones_scale) <- estaciones_cluster$ID # asignamos nombres de estaciones ``` -- - Como estos datos se emplearon en los ejercicios, sabemos que el número óptimo de clústers es de 18. -- - Y configuraremos los centros empleando 100 posiciones iniciales. ```r ### k-means ---- estaciones_k <- kmeans(estaciones_scale, # objeto a clusterizar centers = 18, # centros nstart = 100, # numero de inicios iter.max = 100 ) # para asegurar convergencia del modelo ``` --- ## Paralelización: Aplicación k-means. -- - Nuestro resultado, **estaciones_k**, dispone de un elemento denominado **total within-cluster sum of square** (*estaciones_k$tot.withinss*) para todos nuestros inicios. -- - Recordemos que este valor nos indica que tan buena es nuestra clusterización (debe ser un valor bajo). -- - Ahora veremos algunas alternativas para paralelizar el proceso y visualizar nuestros resultados. <center><img src="data:image/png;base64,#https://raw.githubusercontent.com/allisonhorst/stats-illustrations/main/other-stats-artwork/debugging.jpg" width="500px"/></center> <center>©Allison Horst</center> --- ## 1/2 Paralelización: lapply(). -- - Esta forma de paralelizar está dentro de las más intuitivas y nos dará un acercamiento claro al proceso que realizaremos después. -- - Para esto, construiremos una función de prueba que nos permita paralelizar adecuadamente el cálculo. ```r # creacion de la funcion kmeans_parallel <- function(x) { kmeans(estaciones_scale, # datos directo para simplificar centers = 18, # centros nstart = x, # variable que cambiara iter.max = 100 ) # para asegurar convergencia } ``` .footnote[ ejemplo basado en [Lockwood (2014)](https://www.glennklockwood.com/data-intensive/r/lapply-parallelism.html#3-introduction) y [Orellana (2018)](https://bookdown.org/content/1498/) ] --- ## 1/2 Paralelización: lapply(). -- - Ahora aplicaremos nuestra función, considerando 4 ejecuciones con: 25 como **nstart** para obtener los mismos 100 inicios. ```r # aplicacion funcion con lapply results_k_parallel <- lapply(c(25, 25, 25, 25), FUN = kmeans_parallel) # uso de sapply para extraer valores de tot.withinss temp_vector <- sapply( results_k_parallel, # resultados kmeans function(result) { result$tot.withinss } # funcion ) mejor_resultado <- results_k_parallel[[which.min(temp_vector)]] # seleccion mejor resultado ``` --- ## 1/2 Paralelización: lapply(). <img src="data:image/png;base64,#DIPGEOPR_03_7_files/figure-html/unnamed-chunk-23-1.png" width="80%" /> ``` ## Unit: milliseconds ## expr min lq mean median uq max neval ## serial 50.4247 52.8018 54.83716 54.10075 56.28735 64.0801 100 ## para_lapply 54.1263 57.0370 59.76521 58.66400 61.67580 77.8316 100 ``` --- ## 1/2 Paralelización: lapply(). -- - Lo que realizamos en este ejemplo fue lo siguiente: <center><img src="data:image/png;base64,#lapply_par.png" width="500px"/></center> <center>LabGRS, 2022</center> --- ## Paralelización: librerÃa parallel -- - Como se mencionó anteriormente, existen diversas librerÃas para el procesamiento paralelizado en R. -- - Dos de las más conocidas y poderosas para esta tarea son **multicore** y [**snow**](https://CRAN.R-project.org/package=snow ). -- - Ambas fueron fusionadas e incluÃdas en la instalación de R base a través de la librerÃa **parallel** desde la versión de R 2.14.0 (2011). -- - Esta librerÃa considera los aspectos de paralelización **Socket** y **Forking** descritos con anterioridad. -- - Incluye versiones paralelizadas de la familia *apply -- - En el siguiente ejemplo usaremos la versión paralelizada de lapply(), **parLapply()** para llevar a cabo la clusterización de nuestros datos. -- - Para usar el estilo de paralelismo de parLapply, debemos emplear la conexión tipo red (**Socket**) mencionada anteriormente. --- ## Paralelización: parLapply() -- - Primer paso: setear los núcleos de trabajo ```r library(parallel) # carga de libreria # detectar el numero de nucleos disponibles nucleos <- detectCores(logical = F) - 1 # logical = F, uso de nucleos fisicos ``` -- - Segundo paso: crear el clúster y exportar los datos y variables necesarias a todos los CPU's de forma explÃcita. ```r cl <- makeCluster(nucleos, type = "PSOCK") # creacion del cluster de procesamiento clusterExport(cl, c("estaciones_scale")) ``` -- - En este caso, solo debemos exportar el dataset para que la función lo utilice. --- ## Paralelización: parLapply() -- - Tercer paso: aplicar función y cerrar el clúster. ```r results_parLapply <- parLapply(cl, # cluster c(25, 25, 25, 25), # parámetros de inicio fun = kmeans_parallel ) # Funcion a aplicar stopCluster(cl) # uso de sapply para extraer valores de tot.withinss temp_vector <- sapply( results_parLapply, # resultados kmeans function(result) { result$tot.withinss } # funcion ) mejor_resultado <- results_parLapply[[which.min(temp_vector)]] # seleccion mejor resultado ``` --- ## Paralelización -- - Si comparamos los resultados obtenidos de los tres tipos de procesamiento, ver una baja importante en el tiempo de procesamiento al paralelizar. <img src="data:image/png;base64,#DIPGEOPR_03_7_files/figure-html/unnamed-chunk-27-1.png" width="100%" /> --- - Lo relevante a considerar cuando se paraleliza un proceso: * Al copiar los datos y variables a cada CPU, se empleará tiempo y memoria. * La creación de los clúster por parte del sistema, también consumirá tiempo. * En el ejemplo, el número de datos trabajado es bajo y no es demasiado el impacto de ejecutar el código sin paralelizar. * **No todo puede ser paralelizado**. --- ## Paralelización: librerÃa foreach y doParallel -- - Ahora veremos el uso de la librerÃa [**foreach**](https://CRAN.R-project.org/package=foreach), que proporciona una forma distinta de ejecutar procesos iterativos en R. -- - ¿Si ya existen los ciclos for para qué utilizar esta librerÃa? **Respuesta**: foreach permite la ejecución paralelizada cuando es complementada con la librerÃa [**doParallel**](https://CRAN.R-project.org/package=doParallel). -- - Instale ambas librerÃas empleando `install.packages(c('foreach','doParallel'))` y cárguelas en su entorno de trabajo. ```r library(foreach) library(doParallel) ``` --- ## Paralelización: librerÃa foreach y doParallel -- - Operación básica de foreach: Calculemos la raÃz cuadrada de 3 valores -- ```r foreach(i = 1:3) %do% sqrt(i) ``` ``` ## [[1]] ## [1] 1 ## ## [[2]] ## [1] 1.414214 ## ## [[3]] ## [1] 1.732051 ``` -- - La función **foreach()** se parece a un ciclo for, pero se implementa con el operador ` %do%`. -- - Además, nos devuelve un valor que por defecto es una lista. Esto nos da la flexibilidad de complementar la salida con otros flujos de trabajo. --- ## Paralelización: librerÃa foreach y doParallel -- - Veamos otro ejemplo con dos variables: -- ```r foreach(a = 1:3, b = rep(10, 3)) %do% { a + b } ``` ``` ## [[1]] ## [1] 11 ## ## [[2]] ## [1] 12 ## ## [[3]] ## [1] 13 ``` -- - Aquà **a** y **b** son *"variables de iteración"* dado que cambian durante las múltiples ejecuciones. --- ## Paralelización: librerÃa foreach y doParallel -- - La salida por defecto de foreach es una **lista**, pero podemos especificar otros tipos de salida empleando el argumento `.combine` ```r # vector foreach(i = 1:3, .combine = "c") %do% exp(i) ``` ``` ## [1] 2.718282 7.389056 20.085537 ``` ```r # matriz foreach(i = 1:2, .combine = "cbind") %do% rnorm(4) ``` ``` ## result.1 result.2 ## [1,] -0.4344060 -1.50734742 ## [2,] 1.4078410 0.80664237 ## [3,] 0.5966189 0.77572367 ## [4,] -1.4647919 0.04880043 ``` -- - De igual forma, podemos emplear **rbind()** o incluso funciones propias apliquen algo a nuestros resultados intermedios. --- ## Paralelización: librerÃa foreach y doParallel -- - En la práctica, foreach es equivalente al paralelismo basado en lapply revisado anteriormente. -- - La diferencia, es que expone la paralelización directamente a nosotros, haciéndolo más intuitivo: * No es necesario evaluar una función en cada objeto de entrada, ya que el código contenido en el cuerpo de **foreach** es el que se ejecuta en cada objeto * El código paralelizado emplea la misma sintáxis a través de todos los entornos, por lo que no hay necesidad de cambiar entre miembros de la familia apply (lapply, parLapply, mclapply) ```r mi_lista <- c(1, 2, 3, 4, 5) # valores output_1 <- lapply(mi_lista, FUN = function(x) { y <- x + 1 y }) output_2 <- foreach(x = mi_lista) %do% { y <- x + 1 y } # comparacion unlist(output_1) == unlist(output_2) ``` ``` ## [1] TRUE TRUE TRUE TRUE TRUE ``` --- ## Paralelización: librerÃa foreach y doParallel -- - Apliquemos por última vez nuestro k-means empleando esta forma de paralelizar ```r results_foreach <- foreach(i = c(25, 25, 25, 25)) %do% { kmeans(x = estaciones_scale, centers = 4, nstart = i) } # uso de sapply para extraer valores de tot.withinss temp_vector <- sapply( results_foreach, # resultados kmeans function(result) { result$tot.withinss } # funcion ) mejor_resultado <- results_foreach[[which.min(temp_vector)]] # seleccion mejor resultado ``` ``` ## Unit: seconds ## expr min lq mean median uq max ## foreach_test 0.0283227 0.0318755 0.03523755 0.03398325 0.0389059 0.0473365 ## neval ## 30 ``` -- - Ahora paralelicemos este foreach. --- ## BibliografÃa complementaria - Bosco, J. (2018). "R Para Principantes". [Ver](https://bookdown.org/jboscomendoza/r-principiantes4/) - Cao, R. y Fernández, R. (2022). "Introducción al procesamiento en paralelo en R". En: "Técnicas de Remuestreo". [Ver](https://rubenfcasal.github.io/book_remuestreo/intro-hpc.html) - Gillespie, C., & Lovelace, R. (2021). "Efficient R programming: a practical guide to smarter programming." O'Reilly Media, Inc. [Ver](https://csgillespie.github.io/efficientR/) - Hijmans, R. J., Bivand, R., Forner, K., Ooms, J., Pebesma, E., & Sumner, M. D. (2022). Package ‘terra’. [Ver](https://brieger.esalq.usp.br/CRAN/web/packages/terra/terra.pdf) - Jones, M. (2017). "Quick Intro to Parallel Computing in R". [Ver](https://nceas.github.io/oss-lessons/parallel-computing-in-r/parallel-computing-in-r.html) - McCallum, E., & Weston, S. (2011). "Parallel R". O'Reilly Media, Inc. - Orellana, J. (2018). "HPC con R para investigadores". [Ver](https://bookdown.org/content/1498/) --- class: inverse middle 